package rwt.msg;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;

import rail.mtl.ada_native.Ada.Strings.Unbounded.Unbounded_String;
import rail.mtl.ada_native.MessageTranslationTypes.*;
import rail.mtl.ada_native.NaturalListTypePkg.naturalListPkg.ListType;
import rail.mtl.ada_native.Standard.AdaString;
import rwt.msg.internal.*;

import com.adacore.ajis.*;

/**
 * Converts RailMessages to and from Json format and byte array format.
 */
public final class AdaNativeMessageConverter
{
	private MethodDictionary methods;
	
	private Class<? extends Annotation> splitMethodOnlyFieldAnnotation;
	private Class<? extends Annotation> makeMethodOnlyFieldAnnotation;
	private Class<? extends Annotation> splitMethodOnlyMessageAnnotation;
	private Class<? extends Annotation> makeMethodOnlyMessageAnnotation;
	
	/**
	 * Constructs a new message adaptor.
	 */
	public AdaNativeMessageConverter()
	{
		try
		{
			rail.mtl.ada_native.Ada2Java.Library.load();
		}
		catch (Exception e)
		{
			throw e;
		}
		methods = new MethodDictionary();
	}
	
	public void setAnnotations(Class<? extends Annotation> splitMethodOnlyFieldAnnotation,
			Class<? extends Annotation> makeMethodOnlyFieldAnnotation,
			Class<? extends Annotation> splitMethodOnlyMessageAnnotation,
			Class<? extends Annotation> makeMethodOnlyMessageAnnotation)
	{
		this.splitMethodOnlyFieldAnnotation = splitMethodOnlyFieldAnnotation;
		this.makeMethodOnlyFieldAnnotation = makeMethodOnlyFieldAnnotation;
		this.splitMethodOnlyMessageAnnotation = splitMethodOnlyMessageAnnotation;
		this.makeMethodOnlyMessageAnnotation = makeMethodOnlyMessageAnnotation;
	}
	
	/**
	 * Converts a message to a byte array. The returned byte array follows the specification of the LocoNet or
	 * Controller software.
	 * 
	 * @throws IOException
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 */
	public <T> byte[] toBytes(T message) throws IOException, IllegalAccessException
	{
		// Calls the Ada MTL Make method
		
//		if (message.id == BaseMessage.INVALID_ID)
//			throw new IOException("Message ID is invalid");
		
		List<Object> args = new ArrayList<>();
		Class<?> messageClass = message.getClass();
		Field[] fields = messageClass.getDeclaredFields();
		
		for (Field field : fields)
		{
			// We're preparing to call the message's native Make function, so omit Split-only fields
			if (field.isAnnotationPresent(splitMethodOnlyFieldAnnotation))
				continue;
			
			Class<?> fieldType = field.getType();
			if (fieldType == int.class || fieldType == boolean.class)
			{
				args.add(field.get(message));
			}
			else if (fieldType == List.class)
			{
				ParameterizedType genType = (ParameterizedType) field.getGenericType();
				Type listElementType = genType.getActualTypeArguments()[0];
				if (listElementType != Integer.class)
				{
					throw new IllegalArgumentException("List field '" + field.getName()
							+ "' in message class '" + messageClass.getName()
							+ "' must be a list of integers.");
				}
				
				@SuppressWarnings("unchecked")
				List<Integer> javaList = (List<Integer>) field.get(message);
				ListType adaList = AdaTypeUtil.convertJavaListToAda(javaList);
				args.add(adaList);
			}
			else if (fieldType == String.class)
			{
				String javaString = field.get(message).toString();
				AdaString adaString = new AdaString(javaString);
				args.add(adaString);
			}
			else
			{
				throw new IllegalArgumentException("Type '" + fieldType.getName() + "' not supported.");
			}
		}
		
		try
		{
			Method makeMethod = methods.getMakeMethod(messageClass.getSimpleName());
			MessageType adaMessage = (MessageType) makeMethod.invoke(null, args.toArray());
			
			byte[] bytes = adaMessageToBytes(adaMessage);
			return bytes;
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		
		return null;
	}
	
	/**
	 * Converts a byte array from the LocoNet or Controller to a message object.
	 */
	public <T> T fromBytes(byte[] bytes, Class<T> messageClass)
	{
		// Calls the Ada MTL Split method
		
		int opCode = (int) bytes[0] & 0xff;
		if (opCode == 0)
		{
			// Extended message
			opCode = (int) bytes[1] & 0xff;
		}
		
		if (messageClass == null)
		{
			System.err.println("Railroad message with op-code '" + opCode + " (0x"
					+ Integer.toHexString(opCode) + ")' " + "is not bound to a message class");
			return null;
		}
		
		Field[] fields = messageClass.getDeclaredFields();
		List<Field> splitFields = new ArrayList<>(fields.length);
		List<Object> args = new ArrayList<>(fields.length + 1);
		MessageType adaMessage = bytesToAdaMessage(bytes);
		args.add(adaMessage);
		
		for (Field field : fields)
		{
			// Ignore fields marked as "MakeMethodOnly"
			if (field.isAnnotationPresent(makeMethodOnlyFieldAnnotation))
				continue;
			
			splitFields.add(field);
			Class<?> fieldType = field.getType();
			
			if (fieldType == int.class)
			{
				args.add(new IntegerRef());
			}
			else if (fieldType == boolean.class)
			{
				args.add(new BooleanRef());
			}
			else if (fieldType == List.class)
			{
				args.add(new ListType());
			}
			else if (fieldType == String.class)
			{
				args.add(new Unbounded_String());
			}
			else
			{
				throw new IllegalArgumentException("Type '" + fieldType.getName() + "' not supported.");
			}
		}
		
		try
		{
			T message = messageClass.newInstance();
			Method splitMethod = methods.getSplitMethod(messageClass.getSimpleName());
			
			if (splitMethod == null)
			{
				System.err.println("Ada MessageTranslationLibrary has no Split procedure for \"" + 
						messageClass.getSimpleName() + "\". Ignoring message.");
				return null;
			}
			
			Object[] argArray = args.toArray();
			splitMethod.invoke(null, argArray);
			
			for (int i = 0; i < splitFields.size(); i++)
			{
				Field field = splitFields.get(i);
				Object refObject = argArray[i + 1];
				Class<?> fieldType = field.getType();
				
				if (fieldType == int.class)
				{
					IntegerRef intRef = (IntegerRef) refObject;
					field.setInt(message, intRef.getValue());
				}
				else if (fieldType == boolean.class)
				{
					BooleanRef intRef = (BooleanRef) refObject;
					field.setBoolean(message, intRef.getValue());
				}
				else if (fieldType == List.class)
				{
					List<Integer> javaList = AdaTypeUtil.convertAdaListToJava((ListType) refObject);
					field.set(message, javaList);
				}
				else if (fieldType == String.class)
				{
					@SuppressWarnings("unused")
					Unbounded_String adaString = (Unbounded_String) refObject;
					
					// TODO: Get the string out of Unbounded_String
					throw new RuntimeException("Not yet implemented");
				}
				else
				{
					throw new IllegalArgumentException("Type '" + fieldType.getName() + "' not supported.");
				}
			}
			
			return message;
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		
		return null;
	}
	
	// Private methods =========================================================
	
	private MessageType bytesToAdaMessage(byte[] rawBytes)
	{
		MessageType adaMsg = new MessageType();
		byteArrayType byteArray = adaMsg.byteArray();
		
		for (int i = 0; i < rawBytes.length; i++)
		{
			int value = (int) rawBytes[i] & 0xFF;
			byteArray.Set_Element_At(i + 1, value); // Ada uses one-based indexes
		}
		adaMsg.Size(rawBytes.length);
		
		return adaMsg;
	}
	
	private byte[] adaMessageToBytes(MessageType adaMsg)
	{
		int len = adaMsg.Size();
		byte[] rawBytes = new byte[len];
		byteArrayType byteArray = adaMsg.byteArray();
		
		for (int i = 1; i <= len; i++)
		{
			rawBytes[i - 1] = (byte) byteArray.Get_Element_At(i);
		}
		
		return rawBytes;
	}
}
